// ============================== Configurations Start ==============================
/**
 * PLC 的相关配置，生产环境中，请根据现场的实际情况进行配置。
 * 
 * 【注意】
 *  - 配置 PLC 的参数之前，请先用 ModbusTcp 调试工具，例如 Modbus Poll，对 PLC 进行调试，以确认 PLC 的相关参数是否正常！！！
 * 
 */
const plcConfig = {
    host: "127.0.0.1",              // PLC 的 IP 地址
    port: 5020,                     // PLC 监听的端口号
    slaveId: 1,                     // PLC 中设置的 slaveId。
    enterRequestAddr: {             // “AGV 请求进入光栅”的信号的地址信息
        type: "0x",                 // 地址类型
        number: 0,                  // 地址编号
        mark: "AGV请求进入光栅"      // 地址说明
    },
    enterPermittedAddr: {           // “AGV 可以进入光栅”的信号的地址信息
        type: "1x",
        number: 0,
        mark: "AGV可以进入光栅"
    },
    enableRequestAddr: {            // “AGV 请求打开光栅”的信号的地址信息
        type: "0x",
        number: 1,
        mark: "AGV请求打开光栅"
    },
    rasterEnabledAddr: {            // “AGV 可以离开光栅”的信号的地址信息
        type: "1x",
        number: 1,
        mark: "AGV可以离开"
    }
}

// 系统正在使用的天风任务的名称，请勿随意修改！
const windTaskTransfer = "transfer"
// 机器人名字
const vehicles = ["sim_01"]
// 停止运动请求地址
const softEmergencyStopUrl = "http://localhost:8088/"
// 逻辑库位名称(管理PLC异常情况)
const logicSite = "逻辑PS-X-01"

// ------------------------------  Configurations End  ------------------------------

// ==========================================================================================================
// ============================== 以下内容为脚本代码逻辑，未经允许，请勿修改！！！ ==============================
// ==========================================================================================================
function boot() {
    initEvent()
    initHttpHandler()
}

// ============================== Events Start ==============================
function initEvent() {
    jj.registerTaskEventFunction("onTaskFinished")
    // 是否已发送急停请求
    jj.putCacheParam("softEmergencyStopStatus", "0")
    // 设置逻辑库位状态
    jj.setFilledWithContentBySiteId(logicSite, "2")
    jj.defineScheduledFunctions(true, 5000, 500, "softEmergencyStop", [])
}

function onTaskFinished(param: string) {
    logInfo(param)

    const paramObject = JSON.parse(param)
    const taskRecord = paramObject["taskRecord"]
    const taskRecordId = taskRecord["id"]

    const taskLabel = taskRecord["defLabel"]
    logInfo("taskLabel: " + taskLabel)
    if (taskLabel !== windTaskTransfer) {
        logInfo("not customed task")
        return
    }

    logInfo("customed task")

    // 任务异常结束时，解锁起点和终点库位
    const status = taskRecord["status"]         // 1000：运行中，1001：终止，1002：暂停，1003：结束，1004：异常结束
    logInfo("taskStatus: " + status)
    if (status === 1001 || status === 1004) {
        const inputParams: any[] = JSON.parse(taskRecord["inputParams"])
        // 0: 起点和终点都还未被解锁；1: 仅解锁了起点；2: 起点和终点都被解锁了
        const unlockStatus = getInputParamsValue(inputParams, "unlockStatus")
        const bySql = false
        logInfo("unlockStatus: " + unlockStatus)
        if (unlockStatus === 0) {
            unlockSiteIfTaskFinishedAbnormally(taskRecordId, true, inputParams, bySql)
            unlockSiteIfTaskFinishedAbnormally(taskRecordId, false, inputParams, bySql)
        } else if (unlockStatus === 1) {
            unlockSiteIfTaskFinishedAbnormally(taskRecordId, true, inputParams, bySql)
        }
    }

    // TODO: 任务异常结束时，复位光栅的信号。
}

function softEmergencyStop() {
    const siteStr = jj.getSiteById(logicSite)
    var site = JSON.parse(siteStr)
    if (site.content == "2") {
        return
    } else if (site.content == "0") {
        logInfo("机器人将进入光栅")
    } else {
        logInfo("机器人已进入光栅")
    }
    var enable = jj.readSingleModbusValue(plcConfig.host, plcConfig.port, plcConfig.slaveId, plcConfig.enterPermittedAddr.type, plcConfig.enterPermittedAddr.number)
    let req: {
        vehicles: any
    }
        = {
        vehicles: vehicles
    }
    var softEmergencyStopStatus = jj.getCacheParam("softEmergencyStopStatus")
    if (enable == null || enable == 0) {
        if (softEmergencyStopStatus == "0") {
            jj.requestPost(`${softEmergencyStopUrl}gotoSitePause`, JSON.stringify(req))
            jj.putCacheParam("softEmergencyStopStatus", "1")
        }
        return
    }
    if (softEmergencyStopStatus == "1") {
        jj.requestPost(`${softEmergencyStopUrl}gotoSiteResume`, JSON.stringify(req))
        jj.putCacheParam("softEmergencyStopStatus", "0")
    }
}

//设置逻辑库位内容，判断光栅状态
function setStatus(param: string) {
    var paramJson = JSON.parse(param);
    var type = paramJson["eventData"]
    jj.setFilledWithContentBySiteId(logicSite, type)
}


function unlockSiteIfTaskFinishedAbnormally(taskRecordId: string, fromSite: boolean, inputParams: any, bySql: boolean) {
    const siteId = getInputParamsValue(inputParams, fromSite ? "fromSiteId" : "toSiteId")
    const siteMark = fromSite ? "起点" : "终点"
    const optDescription = `任务 ${taskRecordId} 异常结束时，解锁${siteMark}库位【${siteId}】`
    let message = `${optDescription}成功`
    if (typeof (siteId) === "string" && siteId) {
        if (bySql) {
            const sql = `UPDATE t_worksite SET locked=0, locked_by='' WHERE site_id='${siteId}' AND locked=1 AND locked_by='${taskRecordId}'`
            if (!jj.jdbcExecuteSql(sql)) message = `${optDescription}失败`
        } else {
            const result = unlockSite(siteId, siteMark, taskRecordId, true)
            if (result !== null) message = `${optDescription}失败`
        }
    } else {
        message = `${optDescription}失败，非法的库位ID。`
    }
    logInfo(message)
}
// ------------------------------  Events End  ------------------------------


// ============================== Handles Start ==============================
function initHttpHandler() {
    jj.registerHandler("POST", "/script-api/create-task", "handleCreateTask", false)
}

function handleCreateTask(bodyStr: string) {
    // 创建任务ID
    const taskRecordId = jj.createUuid()
    let lockState = 0       // 0: 还未锁定库位；1: 只锁定了起点；2: 锁定了起点和终点
    let fromSiteId = ""
    let toSiteId = ""
    try {
        logInfo(`create task: ${bodyStr}`)
        const body = JSON.parse(bodyStr)

        const params = body["params"]
        fromSiteId = params[0]["value"]
        toSiteId = params[1]["value"]

        // 判断起点是否存在，是否未被锁定
        const fromErrMsg = checkSiteAvailable(fromSiteId, "起点")
        if (fromErrMsg !== null) return failedResponse(fromErrMsg)

        // 判断终点是否存在，是否未被锁定
        const toErrMsg = checkSiteAvailable(toSiteId, "终点")
        if (toErrMsg !== null) return failedResponse(toErrMsg)

        // 根据起点和终点判断任务是否被重复创建
        const repeatedErrMsg = checkTaskRecordRepected(fromSiteId, toSiteId)
        if (repeatedErrMsg !== null) return failedResponse(repeatedErrMsg)

        // 锁定起点
        const lockFromErrMsg = lockSite(fromSiteId, "起点", taskRecordId)
        if (lockFromErrMsg !== null) return failedResponse(lockFromErrMsg)
        lockState = 1

        // 锁定终点
        const lockToErrMsg = lockSite(toSiteId, "终点", taskRecordId)
        if (lockToErrMsg !== null) return failedResponse(lockToErrMsg)
        lockState = 2

        // 有光栅的库位(PS-X-01)，要进行信号交互
        const checkRaster = (toSiteId === "PS-X-01")

        // 解锁起点库位之后，需要在天风任务中，将 unlockStatus 置为 1。
        // 解锁终点库位之后，需要在天风任务中，将 unlockStatus 置为 2。
        const unlockStatus = 0

        const outOrderNo = `${fromSiteId}-${toSiteId}`

        const inputParams: any = {
            fromSiteId, toSiteId, outOrderNo, checkRaster, unlockStatus
        }
        if (checkRaster) {
            inputParams.host = plcConfig.host
            inputParams.port = plcConfig.port
            inputParams.slaveId = plcConfig.slaveId
            // AGV 请求进入光栅的地址的信息（请求光栅停止工作）
            inputParams.enterAddrType = plcConfig.enterRequestAddr.type
            inputParams.enterAddrNo = plcConfig.enterRequestAddr.number
            inputParams.enterAddrMark = plcConfig.enterRequestAddr.mark
            // AGV 可以进入光栅的地址信息（光栅已经停止工作）
            inputParams.enterPermittedAddrType = plcConfig.enterPermittedAddr.type
            inputParams.enterPermittedAddrNo = plcConfig.enterPermittedAddr.number
            inputParams.enterPermittedAddrMark = plcConfig.enterPermittedAddr.mark
            // 打开光栅的地址信息（请求光栅继续工作）
            inputParams.enableAddrType = plcConfig.enableRequestAddr.type
            inputParams.enableAddrNo = plcConfig.enableRequestAddr.number
            inputParams.enableAddrMark = plcConfig.enableRequestAddr.mark
            // AGV 可以离开光栅的地址信息（光栅已经继续工作）
            inputParams.rasterEnabledAddrType = plcConfig.rasterEnabledAddr.type
            inputParams.rasterEnabledAddrNo = plcConfig.rasterEnabledAddr.number
            inputParams.rasterEnabledAddrMark = plcConfig.rasterEnabledAddr.mark
        }
        const taskParams = {
            taskRecordId,
            taskLabel: windTaskTransfer,
            inputParams: JSON.stringify(inputParams)
        }

        jj.createWindTask(JSON.stringify(taskParams))
        logInfo(`create task success, body=${bodyStr}`)

        return successResponse()

    } catch (e) {
        switch (lockState) {
            case 1:
                // 解锁起点
                unlockSite(fromSiteId, "起点", taskRecordId, true)
                break
            case 2:
                // 解锁起点和终点
                unlockSite(fromSiteId, "起点", taskRecordId, true)
                unlockSite(toSiteId, "终点", taskRecordId, true)
                break
            default:    // 0
                // 不用解锁库位
                break
        }
        logError(`create task failed, body=${bodyStr}, error=`, e)
        return failedResponse("未知异常导致创建任务失败，详见日志文件...")
    }
}

function successResponse(message: string = "OK"): OperatorResponse {
    return { code: 200, body: JSON.stringify({ code: 200, msg: message }) }
}

function failedResponse(message: string): OperatorResponse {
    logError(message, "")
    return { code: 200, body: JSON.stringify({ code: 300, msg: message }) }
}

/*
{
    // HTTP 响应码，只有当值为 200 时，手持端才能解析 body 字段的数据
    "code": 200,

    // boyd.code: 请求的响应码，200 表示请求成功，300 表示请求失败，请求失败时，手持端会显示 msg 的内容。
    // body.message: 响应的内容。
    "body": "{\"code\": ${code},\"msg\": ${msg}}"
}
*/
interface OperatorResponse {
    code: number        // HTTP 响应码，为固定值 200，否者手持端无法解析响应正文。
    body: string        // JSON.stringify(ResponseBody object)
}

interface ResponseBody {
    code: number        // 200: 请求成功; 300: 请求失败，手持端会显示 msg 的内容。
    msg: string         // 默认值为 "OK"
}
// ------------------------------  Handles End  ------------------------------

// ============================== Utils Start ==============================
/**
 * 判断目标库位是否是未被锁定的有效库位
 * @param siteId 目标库位的名称
 * @param siteMark 目标库位的描述
 * @returns 如果是未被锁定的有效库位，则返回null；否则返回异常信息
 */
function checkSiteAvailable(siteId: string, siteMark: SiteMark): string | null {
    const conditions = { siteIds: [siteId] }
    const siteResult = jj.findAvailableSitesByCondition(JSON.stringify(conditions), "ASC")
    if (siteResult === "null")
        return `不存在${siteMark}库位【${siteId}】`
    const fromSiteList = JSON.parse(siteResult)
    const count = fromSiteList.length
    if (count === 0) {
        return `不存在${siteMark}库位【${siteId}】`
    } else if (count > 1) {
        return `系统中存在重复的${siteMark}库位【${siteId}】`
    } else {
        const site = fromSiteList[0]
        if (site.locked) {
            return `${siteMark}库位【${siteId}】已经被其他任务【${site.lockedBy || "-"}】锁定`
        } else {
            return null
        }
    }
}

/**
 * 判断是否存在未结束的从 fromSiteId 到 toSiteId 的任务，防止重复下单。
 * @param fromSiteId 任务的起点库位名称
 * @param toSiteId 任务的终点库位名称
 * @returns 不存在则返回null，否则返回异常信息。
 */
function checkTaskRecordRepected(fromSiteId: string, toSiteId: string): string | null {
    const outOrderNo = `${fromSiteId}-${toSiteId}`
    // 查询所有从 fromSiteId 到 toSiteId ，且还未结束的天风任务实例
    const sql = `SELECT count(1) FROM t_windtaskrecord WHERE out_order_no='${outOrderNo}' AND status IN (1000, 1002)`
    const matchedtaskRecordsCount = jj.jdbcQueryCount(sql)
    if (matchedtaskRecordsCount === null)
        return `查询任务失败：${sql}`
    else if (matchedtaskRecordsCount > 0)
        return `存在【${matchedtaskRecordsCount}】条从【${fromSiteId}】到【${toSiteId}】，且还未结束的任务，请勿重复下单！`
    else return null
}

/**
 * 锁定目标库位
 * @param siteId 目标库位的名称
 * @param siteMark 目标库位的描述
 * @returns 成功时，返回null；失败时，返回异常信息，或者抛异常
 */
function lockSite(siteId: string, siteMark: SiteMark, lockedBy: string): string | null {
    // 此方法未在 catch 中被调用，需要抛异常，sliently = false
    return changeSiteLockedState(siteId, siteMark, true, lockedBy, false)
}

/**
 * 解锁目标库位
 * @param siteId 目标库位的名称
 * @param siteMark 目标库位的描述
 * @param sliently true:此方法不抛异常，但是会返回异常信息；false:此方法会抛异常
 * @returns 成功时，返回null；失败时，返回异常信息，或者抛异常
 */
function unlockSite(siteId: string, siteMark: SiteMark, lockedBy: string, sliently: boolean): string | null {
    // 此方法在 catch 中被调用时，不能抛异常，sliently = true
    // 此方法在 try 中被调用时，可以抛异常，sliently = false
    return changeSiteLockedState(siteId, siteMark, false, lockedBy, sliently)
}

/**
 * 修改库位的锁定状态
 * @param siteId 目标库位的名称
 * @param siteMark 目标库位的描述
 * @param lockIt true:执行锁定库位的操作；false:执行解锁库位的操作
 * @param sliently true:此方法不抛异常，但是会返回异常信息；false:此方法会抛异常
 * @returns 成功时，返回null；失败时，返回异常信息，或者抛异常
 */
function changeSiteLockedState(siteId: string, siteMark: SiteMark, lockIt: boolean, lockedBy: string, sliently: boolean): string | null {
    const optMark = lockIt ? `锁定${siteMark}` : `解锁${siteMark}`
    const optDescription = `${optMark} 库位【${siteId}】`
    try {
        const lockedByCondition = lockIt ? "" : lockedBy
        const lockedByNewValue = lockIt ? lockedBy : ""
        const conditions = { siteIds: [siteId], lockedBy: lockedByCondition }
        const newValues = { locked: lockIt, lockedBy: lockedByNewValue }
        const count = jj.updateSitesByCondition(JSON.stringify(conditions), JSON.stringify(newValues))
        if (count <= 0) {
            if (!lockIt) return `${optDescription}失败`
            else {
                return lockSiteIfLockedByIsNull(conditions, newValues, optDescription)
            }
        } else if (count > 1) {
            return `${optDescription}失败，更新了【${count}】条记录`
        } else {
            logInfo(`${optDescription}成功`)
            return null
        }
    } catch (e) {
        logError(optDescription, e)
        if (sliently) return `${optDescription} 失败，详见日志文件...`
        else throw e
    }
}

/**
 * 当库位的 lockedBy 字段为 null 时，尝试锁定库位
 * @param conditions 锁定库位的条件，函数中会强制指定 conditions.lockedBy=null
 * @param newValues 库位被锁定之后的状态。
 * @param remark 操作的描述
 * @returns 操作成功时，返回null；操作失败时，返回异常信息
 */
function lockSiteIfLockedByIsNull(conditions: any, newValues: any, remark: string): string | null {
    conditions.lockedBy = null
    const conditionsStr = JSON.stringify(conditions)
    const newValuesStr = JSON.stringify(newValues)
    logInfo(`try to lock site when it's locked by null, conditions=${conditionsStr}, newValue=${newValuesStr}`)
    const count = jj.updateSitesByCondition(conditionsStr, newValuesStr)
    if (count <= 0) {
        return `${remark}失败`
    } else if (count > 1) {
        return `${remark}，更新了【${count}】条记录`
    } else {
        logInfo(`${remark}成功`)
        return null
    }
}

function getInputParamsValue(inputParams: any[], failedName: string): any {
    return inputParams.find(i => i.name === failedName).defaultValue
}

function getEnterRequestAddr(): any {
    return plcConfig.enterRequestAddr
}

function getEnterPermittedAddr(): any {
    return plcConfig.enterPermittedAddr
}

function getEnableRequestAddr(): any {
    return plcConfig.enableRequestAddr
}

function getRasterEnabledAddr(): any {
    return plcConfig.rasterEnabledAddr
}

/**
 * 记录消息到后台的日志文件中
 * @param message 消息内容
 */
function logInfo(message: string) {
    jj.getLogger().info(message)
}

/**
 * 记录异常信息到后台的日志文件中
 * @param message 消息内容
 * @param e 异常
 */
function logError(message: string, e: any) {
    jj.getLogger().error(message, e)
}

type SiteMark = "起点" | "终点"
// ------------------------------  Utils End  ------------------------------
